﻿using System;
using System.Collections.Generic;
using System.Linq;
using AMO = Microsoft.AnalysisServices;

namespace AmoExtensions
{
    /*
     * SSAS Partition Manager
     * https://ssaspartitionmanager.codeplex.com/
     * Written by Dr. John Tunnicliffe of Decision Analytics, April 2015
     * eMail: john@decision-analytics.co.uk 
     * http://www.decision-analytics.co.uk/
     * https://www.linkedin.com/in/drjohntunnicliffe
     */

    /// <summary>
    /// PartitionManager class obtains the list of partitions from the SQL database, 
    /// creates new partitions, deletes redundant partitions and commits the changes to the cube. 
    /// </summary>
    public class PartitionManager : IPartitionManager
    {
        #region Properties and Constants

        private const string MustCallGetListOfPartitions = "You must call populate ListOfPartitions yourselve or call GetListOfPartitions before invoking {0}";

        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private readonly IAmoServer server;

        public readonly IEnumerable<IPartition> ListOfPartitions;

        private bool cubesNeedsUpdating = false;
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the PartitionManager class.
        /// </summary>
        /// <param name="listOfPartitions">A list of partitions to manage</param>
        public PartitionManager(IAmoServer server, IEnumerable<IPartition> listOfPartitions)
        {
            // check server parameter
            if (server == null) throw new ArgumentNullException("server");
            if (!server.Connected) throw new ApplicationException(Partition.SsasServerNotConnected);

            // check listOfPartitions parameter
            if (listOfPartitions == null) throw new ArgumentNullException("listOfPartitions");
            if (listOfPartitions.Count() == 0)
                throw new ApplicationException("No partitions listed in listOfPartitions!");

            this.ListOfPartitions = listOfPartitions;
            this.server = server;

        }
        #endregion        

        #region IsPartitionListValid
        /// <summary>
        /// Validate that the list of partitions we got from the server is sensible
        /// 1. check the databases exist
        /// 2. check that the cubes exist
        /// 3. check the measure groups exist
        /// If any partition definition is invalid, it will be logged and marked as invalid so it is subsequently 
        /// ignored by the CreatePartitions / DeletePartitions steps
        /// </summary>
        /// <returns>true if the partition list is valid</returns>
        public bool IsPartitionListValid()
        {
            if (ListOfPartitions == null)
            {
                throw new ApplicationException(string.Format(MustCallGetListOfPartitions, "IsPartitionListValid()"));
            }

            bool returnValue = true;  // assume the list is valid unless we find otherwise

            foreach (Partition partition in ListOfPartitions)
            {
                try
                {
                    partition.IsValid();
                }
                catch (Exception ex)
                {
                    string msg = string.Format("Invalid partition definition for database '{0}' cube '{1}' measure group '{2}'. Reason: {3}",
                        partition.DatabaseName,
                        partition.CubeName,
                        partition.MeasureGroupName,
                        ex.Message);

                    log.Error(msg);

                    returnValue = false;
                }
            }
            return returnValue;
        }
        #endregion

        #region CreatePartitions
        /// <summary>
        /// Creates the partitions if are are missing
        /// </summary>
        public void CreatePartitions()
        {
            if (ListOfPartitions == null)
            {
                throw new ApplicationException(string.Format(MustCallGetListOfPartitions, "CreatePartitions()"));
            }

            // This will create any partitions that do not already exist in the cube
            foreach (Partition partition in ListOfPartitions.Where(x => x.IsValidPartitionDefinition == true))
            {
                if (partition.Create())
                {
                    cubesNeedsUpdating = true;
                }
            }
        }
        #endregion

        #region DeletePartitions
        /// <summary>
        /// Deletes partitions from the cube if they are not listed in the list of partitions
        /// </summary>
        public void DeletePartitions()
        {
            if (ListOfPartitions == null)
            {
                throw new ApplicationException(string.Format(MustCallGetListOfPartitions, "DeletePartitions()"));
            }

            List<AMO.Partition> PartitionsToDelete = new List<AMO.Partition>();

            // First find each database we are working on
            foreach (string SsasDatabaseName in ListOfPartitions
                .Where(x => x.IsValidPartitionDefinition == true)
                .Select(x => x.DatabaseName).Distinct())
            {
                // Next find the cubes within the database we are working on
                foreach (string SsasCubeName in ListOfPartitions
                       .Where(x => x.DatabaseName == SsasDatabaseName && x.IsValidPartitionDefinition == true)
                       .Select(x => x.CubeName).Distinct())
                {
                    // Now find the partitioned measure groups within the cube we are working on
                    foreach (string SsasMeasureGroupName in ListOfPartitions
                        .Where(x => x.DatabaseName == SsasDatabaseName && x.CubeName == SsasCubeName && x.IsValidPartitionDefinition == true)
                        .Select(x => x.MeasureGroupName).Distinct())
                    {
                        // The MeasureGroup method does each call seperately so that an error is generated early and 
                        // we can report the error correctly when spurious information is found in the relational database 
                        AMO.MeasureGroup measureGroup = server.MeasureGroup(SsasDatabaseName, SsasCubeName, SsasMeasureGroupName);
                        foreach (AMO.Partition amoPartition in measureGroup.Partitions)
                        {
                            if (!amoPartition.Name.ToLower().Contains("template"))
                            {
                                // now check if the partition appears on the list we retrieved from the spm.OlapPartitions SQL view
                                // if it does not appear in the list, then delete the partition
                                try
                                {
                                    log.DebugFormat("Checking partition {0} is exist in MG: {1} Cube: {2} DB: {3}",
                                            amoPartition.Name,
                                            SsasMeasureGroupName,
                                            SsasCubeName,
                                            SsasDatabaseName
                                    );

                                    // the following line will cause an error if the partition does not exist in _partitions, so we catch the error and delete the offending partition
                                    ListOfPartitions
                                        .Where(x => x.DatabaseName == SsasDatabaseName && x.CubeName == SsasCubeName && x.MeasureGroupName == SsasMeasureGroupName && x.PartitionName == amoPartition.Name)
                                        .First();
                                }
                                catch
                                {
                                    // we have to add the partitions that we wish to delete to a list
                                    // because if we try to delete the partition here, then we get the error "metadata has changed, unable to enumerate object"
                                    PartitionsToDelete.Add(amoPartition);
                                }
                            }
                        }
                    }
                }
            }

            if (PartitionsToDelete.Count > 0)
            {
                log.DebugFormat("Deleting {0} partitions", PartitionsToDelete.Count);

                foreach (AMO.Partition amoPartition in PartitionsToDelete)
                {
                    cubesNeedsUpdating = true;
                    log.WarnFormat("Deleting partition {0}", amoPartition.Name);
                    amoPartition.Parent.Partitions.Remove(amoPartition);
                }
            }
        }
        #endregion

        #region UpdateCubeMetaData
        /// <summary>
        /// Update each cube's metadata for the partitions we just added order deleted.
        /// </summary>
        public void UpdateCubeMetaData()
        {
            if (ListOfPartitions == null)
            {
                throw new ApplicationException(string.Format(MustCallGetListOfPartitions, "UpdateCubeMetaData()"));
            }

            /*
             * Update each cube's metadata for all the partitions we just added.
             * Calling Update() is effectively the same as T-SQL COMMIT TRAN
             * This is a SLOW process, so we do not want to do it after each partition creation or 
             * deletion step, so we centralise it here.
             */

            // only do an update if we have created or deleted a partition
            if (cubesNeedsUpdating)
            {
                // First find each database we have been working on
                foreach (string SsasDatabaseName in ListOfPartitions.Where(x => x.IsValidPartitionDefinition == true).Select(x => x.DatabaseName).Distinct())
                {
                    // now find the cubes within the database we have been working on
                    foreach (string SsasCubeName in ListOfPartitions.Where(x => x.IsValidPartitionDefinition == true && x.DatabaseName == SsasDatabaseName).Select(x => x.CubeName).Distinct())
                    {
                        // update each cube in the database so all the partitions are committed to the cube structure                
                        
                        log.InfoFormat("Updating cube metadata for '{0}' in '{1}'. This may take some time....", SsasCubeName, SsasDatabaseName);
                        server.UpdateCube(SsasDatabaseName, SsasCubeName);
                    }
                }
            } 
            else
                log.Info("Nothing to do!  All cube partitions match the required definition");
        }
        #endregion
    }
}
